/**
 * Project Name:myutils
 * Date:2018年12月19日
 * Copyright (c) 2018, jingma All Rights Reserved.
 */

package cn.benma666.sjsj.web;

import cn.benma666.constants.SysAuth;
import cn.benma666.constants.UtilConst;
import cn.benma666.constants.UtilConstInstance;
import cn.benma666.crypt.DesUtil;
import cn.benma666.dict.Zdlb;
import cn.benma666.domain.SysQxApp;
import cn.benma666.domain.SysQxJgxx;
import cn.benma666.domain.SysQxYhxx;
import cn.benma666.domain.SysSjglSjdx;
import cn.benma666.exception.MyException;
import cn.benma666.exception.NotFindUserException;
import cn.benma666.iframe.*;
import cn.benma666.json.JsonUtil;
import cn.benma666.myutils.DateUtil;
import cn.benma666.myutils.StringUtil;
import cn.benma666.sjzt.Db;
import cn.benma666.sjzt.SjsjField;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.beetl.sql.core.SqlId;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;

import java.net.URLEncoder;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 用户管理器 <br/>
 * date: 2018年12月19日 <br/>
 * @author jingma
 */
public class UserManager extends BasicObject {
    /**
     * 临时用户
     */
    public static final String LSYH = UtilConst.LSYH;
    /**
     * 登陆凭证
     */
    public static final String TOKEN = "token";
    /**
     * 登陆时间KEY
     */
    public static final String DLSJ = "dlsj";

    /**
     * 全部权限信息缓存
     */
    protected static final JSONObject authAllCache = CacheFactory.use("authAll", CacheFactory.TYPE_MEMORY);

    /**
     * redis工具
     */
    private static RedisTemplate<String, Object> redisTemplate;
    /**
     * 会话有效期(分钟)
     */
    private static long sessionYxq = 12*60*60000;
    /**
     * 用户信息缓存
     */
    private static final JSONObject userCache = CacheFactory.use(LjqInterface.KEY_USER);

    public UserManager(RedisTemplate<String, Object> redisTemplate) {
        UserManager.redisTemplate = redisTemplate;
        sessionYxq = DateUtil.scSjStrToLong(Conf.getVal("server.servlet.session.timeout",
                sessionYxq/1000+"S"));
    }

    /**
     * @return 根据token获取用户
     */
    public static SysQxYhxx getUser(String token) {
        MyParams myParams = new MyParams();
        myParams.sys().setToken(token);
        return getUser(myParams);
    }

    /**
     * 获取用户信息 <br/>
     *
     * @param myParams 参数
     * @return 用户信息，当token为空时返回空
     * @author jingma
     */
    public static SysQxYhxx getUser(MyParams myParams) {
        String token = myParams.sys().getToken();
        //已经获取过用户了
        SysQxYhxx user = myParams.user();
        if(user==null){
            if (StringUtil.isBlank(token)) {
                //token一般来说一定会有值，没有值为系统内部调用
                return null;
            }
            synchronized (userCache){
                user = userCache.getObject(token,SysQxYhxx.class);
            }
        }else {
            //直接返回
            return user;
        }
        if(user!=null){
            long dlsj = (long) user.get(DLSJ);
            if(System.currentTimeMillis()-dlsj>sessionYxq){
                //过期
                synchronized (userCache){
                    userCache.remove(token);
                }
                user = null;
            }else{
                //重置有效期
                if(redisTemplate!=null){
                    redisTemplate.expire(LjqInterface.KEY_USER+":"+token,sessionYxq, TimeUnit.MILLISECONDS);
                }
                user.set(DLSJ,System.currentTimeMillis());
            }
        }
        if(user==null&&redisTemplate!=null){
            Object obj = redisTemplate.opsForValue().get(LjqInterface.KEY_USER+":"+token);
            if(obj != null) {
                //本系统过期，或本系统第一次访问，其他子系统还在访问的场景
                user = (SysQxYhxx) obj;
                addUser(token,user);
            }
        }
        //实现免登陆，处理带用户信息的url
        if (user == null || (LSYH.equals(user.getYhdm()) && !isBlank(myParams.sys().getUserInfo()))) {
            //第一次请求或（是临时用户且提供了加密用户信息）
            return zddl(myParams, token,user);
        }
        return user;
    }

    /**
     * 将用户信息加密，然后重定向到指定url <br/>
     * @param url 待转发地址
     * @param projectCode 项目代码
     * @param user 用户标志，默认为用户身份证号，可以采用“yhdm=xxx”的形式传入用户代码进行自动登陆
     * @return 带用户信息的地址
     * @author jingma
     */
    public static String doDesEncryptUrl(String url, String projectCode, String user){
        String token = defaultCache.getString(projectCode + user);
        if(isBlank(token)){
            //避免每次都进行完整的登录
            token=StringUtil.getUUIDUpperStr();
            defaultCache.put(projectCode + user,token);
        }
        String userInfo = enUserinfo(projectCode, user);
        JSONObject app = DictManager.zdObjByDm(Zdlb.SYS_QX_APP.name(), projectCode);
        try {
            url = app.getString("dz") + url;
            int idx = url.indexOf("?");
            if (idx > -1) {
                //解决url中包含中文参数导致转发后中文信息丢失问题。
                StringBuilder url1 = new StringBuilder(url.substring(0, idx + 1));
                for (String p : url.substring(idx + 1).split("&")) {
                    String[] pa = p.split("=");
                    url1.append(pa[0]).append("=").append(URLEncoder.encode(pa[1], "utf8")).append("&");
                }
                url = url1.toString();
            } else {
                url += "?";
            }
            url += "sys.userInfo=" + URLEncoder.encode(userInfo, "utf8")+"&sys.token="+token;
            if(!url.startsWith("http")){
                //配置的相对地址则读取配置的统一代理地址进行补充
                url = Conf.getUtilConfig().getService().getTydlzd()+url;
            }
            return url;
        } catch (Exception e) {
            throw new MyException(projectCode+"生成用户信息转发地址失败：" + e.getMessage());
        }
    }

    /**
     * 加密用户信息
     * @param projectCode 应用代码
     * @param user 用户代码
     * @return 加密串
     */
    public static String enUserinfo(String projectCode, String user) {
        String userInfo;
        userInfo = DateUtil.getGabDate() + "@" + user;
        JSONObject app = DictManager.zdObjByDm(Zdlb.SYS_QX_APP.name(), projectCode);
        if(app ==null){
            throw new MyException("该请求的应用不存在："+ projectCode);
        }
        String pwd = app.getString("mm");
        if (StringUtil.isBlank(pwd)) {
            throw new MyException("应用必须设置密码："+ projectCode);
        }
        pwd = DesUtil.decrypt(pwd, Conf.getUtilConfig().getAppEjmm());
        userInfo = DesUtil.encrypt(userInfo, pwd).replaceAll("[\n\r]", "");
        return userInfo;
    }

    /**
     * 自动登陆 <br/>
     * @param myParams 参数
     * @param token    权限认证key
     * @param oldUser 旧的用户信息
     * @return 登陆后的用户信息
     * @author jingma
     */
    public static SysQxYhxx zddl(MyParams myParams, String token, SysQxYhxx oldUser) {
        if(oldUser!=null&&oldUser.getDlsbcs() >= Conf.getUtilConfig().getLogin().getDlsbcs()) {
            //提前判断，避免爆破
            throw new MyException("你自动登录失败次数过多，请稍后再试");
        }
        SysQxYhxx user = null;
        String userInfo = myParams.sys().getUserInfo();
        if (!StringUtil.isBlank(userInfo)) {
            if(userInfo.startsWith("sqm-")){
                //授权码机制
                userInfo = userInfo.substring(4);
            }else {
                SysQxApp app = DictManager.zdObjByDm(Zdlb.SYS_QX_APP.name(),
                        Conf.getAppdm()).toJavaObject(SysQxApp.class);
                String pwd = app.getMm();
                String zddlms = app.getZddlms();
                if (StringUtil.isBlank(pwd)) {
                    throw new MyException("自动应用必须设置密码", myParams);
                }
                try {
                    //解密应用密码
                    pwd = DesUtil.decrypt(pwd, Conf.getUtilConfig().getAppEjmm());
                } catch (Exception e) {
                    throw new MyException("解析用户信息失败：" + userInfo + ">" + e.getMessage(), e);
                }
                if ("date_user".equals(zddlms)) {
                    //用户时间加密模式，用户信息解密
                    userInfo = DesUtil.decrypt(userInfo, pwd);
                }
                int idx = userInfo.indexOf("@");
                if ("date_user".equals(zddlms)) {
                    //用户时间加密模式，推荐使用
                    Date urlDate = DateUtil.parseDate(userInfo.substring(0, idx));
                    assert urlDate != null;
                    if (Math.abs(urlDate.getTime() - new Date().getTime()) > 1000 * 60 * 5) {
                        throw new MyException("用户信息过期：" + userInfo, myParams);
                    }
                } else if ("pwd_user".equals(zddlms)) {
                    String ipxz = JSON.parseObject(app.getKzxx()).getString("ipxz");
                    if(StringUtil.isBlank(ipxz)){
                        throw new MyException("一般密码模式自动登陆的应用必须配置ip限制：" + userInfo, myParams);
                    }
                    if(!myParams.sys().getClientRealIp().matches(ipxz)){
                        throw new MyException("请在规定的ip机器上访问：" + userInfo, myParams);
                    }
                    if(!pwd.equals(userInfo.substring(0, idx))){
                        //明文密码加用户模式，内部调用
                        throw new MyException("密码不正确：" + userInfo, myParams);
                    }
                }
                userInfo = userInfo.substring(idx + 1);
            }
            if(isBlank(userInfo)){
                throw new MyException("自动登录码为空");
            }
            try {
                user = findUser(userInfo);
                slog.info("自动登陆成功:" + user.getSfzh());
            }catch (NotFindUserException e){
                slog.info("自动登录失败:" + myParams.sys().getUserInfo());
                //找不到用户，这场景要根据ip进行次数限制，避免被爆破
                if(oldUser!=null){
                    //增加失败次数
                    oldUser.setDlsbcs(oldUser.getDlsbcs()+1);
                    //自动登录失败累加失败次数后还是要给出正确提示，之前返回临时用户最终提示没有权限会隐藏真实问题，导致调用方困惑
                    throw new MyException("自动登录失败："+myParams.sys().getUserInfo());
                }
            }
        }
        //没有登录时，返回临时用户，后续可以在权限系统中对临时用户进行授权
        if (user == null) {
            user = findUser(LSYH);
        }
        //采用真实ip
        user.setClientIp(myParams.sys().getClientRealIp());
        if (StringUtil.isNotBlank(user.getXzip())
                && !user.getClientIp().matches(user.getXzip())) {
            slog.info("{}在未授权ip登录{}，{}",user.getYhdm(),user.getXzip(),user.getClientIp());
            throw new MyException("你未不在授权的ip范围内登录：" + user.getYhdm(), myParams);
        } else {
            addUser(token, user);
        }
        if(!isBlank(userInfo)&&LSYH.equals(user.getYhdm())){
            //增加失败次数
            user.setDlsbcs(user.getDlsbcs()+1);
            //如果进行了自动登录，但最终还是临时用户，说明自动登录失败，之所以写到这里是为了存储会话，用于记录失败次数
            throw new MyException("自动登录失败："+myParams.sys().getUserInfo());
        }
        return user;
    }

    /**
     * 添加用户 <br/>
     * @param token 权限码
     * @param user 用户信息
     * @author jingma
     */
    public static void addUser(String token, SysQxYhxx user) {
        qcgqyhhc();
        if (StringUtil.isNotBlank(token)) {
            synchronized (userCache){
                SysQxYhxx oldUser = userCache.getObject(token, SysQxYhxx.class);
                if(oldUser==null||!oldUser.getId().equals(user.getId())){
                    if(LSYH.equals(user.getYhdm())){
                        //临时用户
                        if(getYhhhsByIp(user)>= user.getTszxhhsxz()){
                            //避免恶意者频繁创建新会话来暴力破解系统，ip一般还是有限的
                            slog.info("{}ip同时在线会话数超过限制：{}",user.getClientIp(),user.getTszxhhsxz());
                            throw new MyException("该ip同时在线会话数超过系统限制");
                        }
                    }else if (getYhdlhhs(user) >= user.getTszxhhsxz()) {
                        //若当前登录用户不是已经登录的用户
                        slog.info("{}用户同时在线会话数超过限制：{}",user.getYhdm(),user.getTszxhhsxz());
                        throw new MyException("该用户同时在线会话数超过系统限制");
                    }
                }
                user.setToken(token);
                //设置登陆时间
                user.set(DLSJ,System.currentTimeMillis());
                userCache.put(token,user);
                XtxxWebSocket.setUserByToken(token,user);
                if(redisTemplate!=null){
                    redisTemplate.opsForValue().set(LjqInterface.KEY_USER+":"+token, user, sessionYxq, TimeUnit.MILLISECONDS);
                }
            }
        } else {
            slog.debug("权限码为空：" + user);
        }
    }

    /**
     * @param user 用户
     * @return 用户登录会话数
     */
    public static int getYhdlhhs(SysQxYhxx user){
        int count = 0;
        SysQxYhxx u;
        if(redisTemplate!=null) {
            //使用redis
            for(String key:redisTemplate.keys("user*")){
                u = ((SysQxYhxx) redisTemplate.opsForValue().get(key));
                if(user.getId().equals(u.getId())){
                    count++;
                }
            }
        }else {
            //不使用redis
            for (String key : userCache.keySet()) {
                u = userCache.getObject(key,SysQxYhxx.class);
                if(user.getId().equals(u.getId())){
                    count++;
                }
            }
        }
        return count;
    }

    /**
     * @param user 用户
     * @return 基于ip获取用户在线会话数
     */
    public static int getYhhhsByIp(SysQxYhxx user){
        int count = 0;
        SysQxYhxx u;
        if(redisTemplate!=null) {
            //使用redis
            for(String key:redisTemplate.keys("user*")){
                u = ((SysQxYhxx) redisTemplate.opsForValue().get(key));
                if(user.getClientIp().equals(u.getClientIp())){
                    count++;
                }
            }
        }else {
            //不使用redis
            for (String key : userCache.keySet()) {
                u = userCache.getObject(key,SysQxYhxx.class);
                if(user.getClientIp().equals(u.getClientIp())){
                    count++;
                }
            }
        }
        return count;
    }

    /**
     * 移除登陆的用户，用户登陆退出 <br/>
     *
     * @param user 要移除的用户
     * @return 处理结果
     * @author jingma
     */
    public static Result removeUser(SysQxYhxx user) {
        if(redisTemplate!=null){
            redisTemplate.delete(LjqInterface.KEY_USER+":"+user.getToken());
        }
        synchronized (userCache){
            userCache.remove(user.getToken());
        }
        return success("退出成功");
    }
    public static SysQxYhxx findUser(String userStr) {
        return findUser(userStr,true);
    }
    /**
     * 获取用户信息
     * @param userStr 用户信息字符串，支持id、身份证、代码、微信id
     * @return 用户完整信息
     */
    @NotNull
    public static SysQxYhxx findUser(String userStr,boolean cache) {
        if (userStr.startsWith("yhdm=")) {
            userStr = userStr.substring(5);
        } else if (userStr.startsWith("id=")) {
            userStr = userStr.substring(3);
        }
        MyParams myParams = new MyParams();
        myParams.set("$.yobj.yhdm", userStr);
        return findUser(myParams,cache);
    }
    /**
     * 获取用户的完整信息，包括基础信息、机构、权限 <br/>
     *
     * @param myParams 参数集合
     * @return 用户信息
     * @author jingma
     */
    public static SysQxYhxx findUser(MyParams myParams,boolean cache) throws MyException {
        SysQxYhxx yobj = myParams.yobj(SysQxYhxx.class);
        //设置终端类别，不同终端权限不一样，有些功能手机端不支持
        yobj.setZdlb(myParams.sys().getZdlb());
        Object o = defaultCache.get(yobj.toString());
        if(o!=null&&cache){
            //克隆新对象，避免相互影响
            return JsonUtil.clone((SysQxYhxx)o);
        }
        //查询用户
        SysQxYhxx user;
        user = Db.useSqlManager().selectSingle(SqlId.of("sjsj", "findUser"), myParams, SysQxYhxx.class);
        if(user==null){
            throw new NotFindUserException("没有找到用户："+yobj);
        }
        user.setZdlb(myParams.sys().getZdlb());
        //查询机构
        SysQxJgxx jgxx = Db.use().lambdaQuery(SysQxJgxx.class).andEq(SysQxJgxx::getId,user.getSsjg()).singleSimple();
        if(jgxx==null){
            throw new MyException("没有找到用户的机构："+user);
        }
        user.setJgxx(jgxx);
        loadYhqx(user);
        if(cache){
            //克隆，避免相互影响
            defaultCache.put(yobj.toString(),JsonUtil.clone(user));
        }
        return user;
    }

    /**
     * 刷新用户权限信息 <br/>
     *
     * @author jingma
     * @param myParams 参数对象
     */
    public static void flushUserQxxx(MyParams myParams) {
        SysQxYhxx u;
        synchronized (userCache){
            if(redisTemplate!=null){
                //使用redis
                for(String key:redisTemplate.keys("user*")){
                    u = ((SysQxYhxx) redisTemplate.opsForValue().get(key));
                    loadYhqx(u);
                    userCache.put(u.getToken(),u);
                    redisTemplate.opsForValue().set(key,u,sessionYxq, TimeUnit.MILLISECONDS);
                }
//                final Cursor<String> scan = redisTemplate.scan(ScanOptions.scanOptions().match("user*").build());
//                while (scan.hasNext()){
//                    final String key = scan.next();
//                    user = ((SysQxYhxx) redisTemplate.opsForValue().get(key));
//                }
//                scan.close();
            }else{
                //不使用redis
                for (String key : userCache.keySet()) {
                    u = userCache.getObject(key,SysQxYhxx.class);
                    loadYhqx(u);
                    userCache.put(key,u);
                }
            }
        }
    }

    /**
     * 清除过期用户缓存
     */
    private static void qcgqyhhc(){
        SysQxYhxx user;
        long dlsj;
        synchronized (userCache){
            Iterator<Map.Entry<String, Object>> uc = userCache.entrySet().iterator();
            while (uc.hasNext()) {
                Map.Entry<String, Object> e = uc.next();
                user = (SysQxYhxx) e.getValue();
                dlsj = (long) user.get(DLSJ);
                if(System.currentTimeMillis()-dlsj>sessionYxq){
                    //过期
                    uc.remove();
                }
            }
        }
    }
    /**
     * 加载用户权限相关信息：角色、权限、对象代码与权限的映射
     */
    private static void loadYhqx(SysQxYhxx user){
        JSONObject params = Db.buildKeyMap(UtilConstInstance.KEY_USER, user);
        //查询角色
        user.setJsMap(Db.use().findMap("js",SqlId.of("sjsj","findJsxx"),params));
        //设置用户权限
        user.setQxMap(Db.use().findMap("dm", SqlId.of("sjsj","findYhqxxx"), params));
        //设置用户相关对象与权限代码的映射关系
        user.setDxqxys(Db.use().findMap("dxdm", SqlId.of("sjsj","findYhDxQxYs"), params));
        //设置登陆时间，计算会话超时
        user.set(DLSJ,System.currentTimeMillis());
    }

    public static boolean hasAuth(MyParams myParams){
        return hasAuth(myParams,myParams.sys().getCllx());
    }
    public static boolean hasAuth(MyParams myParams,String cllx){
        return hasAuth(myParams.user(),myParams.sys().getAuthCode(),cllx,
                myParams.sjdx(),myParams.sys().getNbdy());
    }
    public static boolean hasAuth(SysQxYhxx user,String authCode,String cllx){
        return hasAuth(user,authCode+"_"+cllx);
    }
    public static boolean hasAuth(SysQxYhxx user,String qxm){
        return hasAuth(user,qxm,null,false);
    }
    public static boolean hasAuthByUser(JSONObject user,String qxm){
        return hasAuth(user.toJavaObject(SysQxYhxx.class),qxm,null,false);
    }
    public static boolean hasAuth(SysQxYhxx user,String authCode,String cllx,SysSjglSjdx sjdx,boolean nbdy) {
        if (authCode == null) {
            //没有配置权限时则统一采用未授权对象的权限
            authCode = SysAuth.QTQX_WSQDX.name();
        }
        //最终权限码
        String qxm = authCode + "_" + cllx;
        return hasAuth(user, qxm, sjdx, nbdy);
    }
    public static boolean hasAuth(SysQxYhxx user,String qxm,SysSjglSjdx sjdx,boolean nbdy){
        //有权限
        boolean yqx = false;
        if (nbdy||user==null||user.getQxMap().containsKey(SysAuth.QTQX_SYS.name())) {
            //内部调用或内部调用没有用户或拥有最高权限，则默认通过。
            yqx = true;
        }else{
            synchronized (authAllCache){
                if(authAllCache.isEmpty()){
                    //权限缓存为空则读取权限信息并缓存
                    authAllCache.putAll(Db.use().findMap("dm", SqlId.of("sjsj",
                            "findYhqxxx"), Db.buildMap()));
                }
            }
            //没有配置权限的接口登陆用户可以直接访问，配置了权限的接口必须授权才能访问
            if(!authAllCache.containsKey(qxm)&&!UtilConst.LSYH.equals(user.getYhdm())){
                yqx = true;
            }else if (user.getQxMap().containsKey(qxm)) {
                //对象权限判断
                yqx = true;
            }
            String sqms = valByDef(Conf.getUtilConfig().getXtqxSqms(), SjsjField.cllx.name());
            switch (sqms){
                case "sjdx":
                    if(!yqx&&sjdx!=null){
                        //数据对象不为空，为空则走处理类型校验
                        yqx = user.getDxqxys().containsKey(sjdx.getDxdm());
                    }
                    break;
                case "cllx":
                    break;
                default:
                    throw new MyException("暂不支持该授权模式："+sqms);
            }
        }
        return yqx;
    }
}
